#include <SDL/SDL.h>
#include <stdio.h>
#include <setjmp.h>
#include <jpeglib.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include "guacimage.h"

static struct
{
	int loaded;
	void *handle;
	void (*jpeg_calc_output_dimensions) (j_decompress_ptr cinfo);
	void (*jpeg_CreateDecompress) (j_decompress_ptr cinfo, int version, size_t structsize);
	void (*jpeg_destroy_decompress) (j_decompress_ptr cinfo);
	boolean (*jpeg_finish_decompress) (j_decompress_ptr cinfo);
	int (*jpeg_read_header) (j_decompress_ptr cinfo, boolean require_image);
	JDIMENSION (*jpeg_read_scanlines) (j_decompress_ptr cinfo, JSAMPARRAY scanlines, JDIMENSION max_lines);
	boolean (*jpeg_resync_to_restart) (j_decompress_ptr cinfo, int desired);
	boolean (*jpeg_start_decompress) (j_decompress_ptr cinfo);
	struct jpeg_error_mgr * (*jpeg_std_error) (struct jpeg_error_mgr * err);
} lib;

#define INPUT_BUFFER_SIZE	4096
typedef struct {
	struct jpeg_source_mgr pub;

	SDL_RWops *ctx;
	Uint8 buffer[INPUT_BUFFER_SIZE];
} my_source_mgr;

static void init_source (j_decompress_ptr cinfo)
{
	/* We don't actually need to do anything */
	return;
}

/*
 * Fill the input buffer --- called whenever buffer is emptied.
 */
static int fill_input_buffer (j_decompress_ptr cinfo)
{
	my_source_mgr* src = (my_source_mgr*) cinfo->src;
	int nbytes;

	nbytes = SDL_RWread(src->ctx, src->buffer, 1, INPUT_BUFFER_SIZE);
	if (nbytes <= 0) {
		/* Insert a fake EOI marker */
		src->buffer[0] = (Uint8) 0xFF;
		src->buffer[1] = (Uint8) JPEG_EOI;
		nbytes = 2;
	}
	src->pub.next_input_byte = src->buffer;
	src->pub.bytes_in_buffer = nbytes;

	return TRUE;
}


/*
 * Skip data --- used to skip over a potentially large amount of
 * uninteresting data (such as an APPn marker).
 *
 * Writers of suspendable-input applications must note that skip_input_data
 * is not granted the right to give a suspension return.  If the skip extends
 * beyond the data currently in the buffer, the buffer can be marked empty so
 * that the next read will cause a fill_input_buffer call that can suspend.
 * Arranging for additional bytes to be discarded before reloading the input
 * buffer is the application writer's problem.
 */
static void skip_input_data (j_decompress_ptr cinfo, long num_bytes)
{
	my_source_mgr * src = (my_source_mgr *) cinfo->src;

	/* Just a dumb implementation for now.	Could use fseek() except
	 * it doesn't work on pipes.  Not clear that being smart is worth
	 * any trouble anyway --- large skips are infrequent.
	 */
	if (num_bytes > 0) {
		while (num_bytes > (long) src->pub.bytes_in_buffer) {
			num_bytes -= (long) src->pub.bytes_in_buffer;
			(void) src->pub.fill_input_buffer(cinfo);
			/* note we assume that fill_input_buffer will never
			 * return FALSE, so suspension need not be handled.
			 */
		}
		src->pub.next_input_byte += (size_t) num_bytes;
		src->pub.bytes_in_buffer -= (size_t) num_bytes;
	}
}

/*
 * Terminate source --- called by jpeg_finish_decompress
 * after all data has been read.
 */
static void term_source (j_decompress_ptr cinfo)
{
	/* We don't actually need to do anything */
	return;
}

static void jpeg_SDL_RW_src (j_decompress_ptr cinfo, SDL_RWops *ctx)
{
    my_source_mgr* src;

    if (cinfo->src == NULL) // first time
    {
        cinfo->src = (struct jpeg_source_mgr *)(*cinfo->mem->alloc_small)
                        ((j_common_ptr) cinfo, JPOOL_PERMANENT, sizeof(my_source_mgr));
        src = (my_source_mgr*)cinfo->src;
    }

    src = (my_source_mgr*)cinfo->src;
    src->pub.init_source = init_source;
    src->pub.fill_input_buffer = fill_input_buffer;
    src->pub.skip_input_data = skip_input_data;
    src->pub.resync_to_restart = jpeg_resync_to_restart; // default method
    src->pub.term_source = term_source;
    src->ctx = ctx;
    src->pub.bytes_in_buffer = 0; /* forces fill_input_buffer on first read */
    src->pub.next_input_byte = NULL; /* until buffer loaded */
}

// Error handling

struct my_error_mgr {
	struct jpeg_error_mgr errmgr;
	jmp_buf escape;
};

static void my_error_exit(j_common_ptr cinfo)
{
	struct my_error_mgr *err = (struct my_error_mgr *)cinfo->err;
	longjmp(err->escape, 1);
}

static void output_no_message(j_common_ptr cinfo)
{
	/* do nothing */
}

// Parameters: FILE* pointer
// Returns: light userdata to SDL_Surface
int raw_loadjpeg(lua_State* L)
{
    FILE* fp = *(FILE**)lua_touserdata(L,1);

	struct jpeg_decompress_struct cinfo;

	SDL_Surface* surf = NULL;

    struct my_error_mgr jerr;
    cinfo.err = jpeg_std_error(&jerr.errmgr);
    jerr.errmgr.error_exit = my_error_exit;
    jerr.errmgr.output_message = output_no_message;
    if (setjmp(jerr.escape))
    {
        jpeg_destroy_decompress(&cinfo);
        if (surf)
            SDL_FreeSurface(surf);
        return luaL_error(L, "jpeg loading error");
    }

    jpeg_create_decompress(&cinfo);
    SDL_RWops* src = SDL_RWFromFP(fp, 0);
    jpeg_SDL_RW_src(&cinfo, src);
    jpeg_read_header(&cinfo, TRUE);

	if (cinfo.num_components == 4)
	{
		/* Set 32-bit Raw output */
		cinfo.out_color_space = JCS_CMYK;
		cinfo.quantize_colors = FALSE;
		jpeg_calc_output_dimensions(&cinfo);

		/* Allocate an output surface to hold the image */
		surf = SDL_AllocSurface(SDL_SWSURFACE, cinfo.output_width, cinfo.output_height, 32,
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
            0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000);
#else
            0x0000FF00, 0x00FF0000, 0xFF000000, 0x000000FF);
#endif
    }
    else
    {
		/* Set 24-bit RGB output */
		cinfo.out_color_space = JCS_RGB;
		cinfo.quantize_colors = FALSE;
#ifdef FAST_JPEG
		cinfo.scale_num   = 1;
		cinfo.scale_denom = 1;
		cinfo.dct_method = JDCT_FASTEST;
		cinfo.do_fancy_upsampling = FALSE;
#endif
		jpeg_calc_output_dimensions(&cinfo);

		/* Allocate an output surface to hold the image */
		surf = SDL_AllocSurface(SDL_SWSURFACE, cinfo.output_width, cinfo.output_height, 24,
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
            0x0000FF, 0x00FF00, 0xFF0000, 0);
#else
            0xFF0000, 0x00FF00, 0x0000FF, 0);
#endif
	}

	if (!surf)
	{
		jpeg_destroy_decompress(&cinfo);
		return luaL_error(L, "out of memory");
	}

	JSAMPROW rowptr[1];

	/* Decompress the image */
	jpeg_start_decompress(&cinfo);
	while (cinfo.output_scanline < cinfo.output_height)
	{
		rowptr[0] = (JSAMPROW)(Uint8 *)surf->pixels + (cinfo.output_scanline * surf->pitch);
		jpeg_read_scanlines(&cinfo, rowptr, (JDIMENSION)1);
	}
	jpeg_finish_decompress(&cinfo);
	jpeg_destroy_decompress(&cinfo);

    GuacImage* img = (GuacImage*)lua_newuserdata(L, sizeof(GuacImage));
    img->surf = surf;
    img->prect = NULL;
    img->flags = 0;
    img->handle_x = img->handle_y = 0;

    return 1;
}
